﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using Microsoft.VisualStudio.TextManager.Interop;
using Microsoft.VisualStudio.OLE.Interop;
using Microsoft.VisualStudio.Shell;
using EnvDTE;

namespace NickPowell.LiveCode
{
    public class Document : IVsTextLinesEvents
    {
        private string sRelativePath = "";
        private Project project;

        private IVsTextView textView;

        /// <summary>
        /// Contains the next text sequence expected from each user
        /// </summary>
        private Dictionary<int, int> dicUserTextSequences = new Dictionary<int, int>();

        /// <summary>
        /// A list of text changes that haven't been consumed yet
        /// </summary>
        private List<TextChange> lstTextChanges = new List<TextChange>();

        private bool bIgnoreChange = false;
        /// <summary>
        /// Set to ignore multiple changes at one instead of just one
        /// </summary>
        private bool bIgnoreChanges = false;

        public string RelativePath
        {
            get
            {
                return this.sRelativePath;
            }
        }

        public Document(string _sRelativePath, Project _project)
        {
            this.sRelativePath = _sRelativePath;
            this.project = _project;

            this.StartTextEventListening();
        }

        public void EditText(XmlDocument xmlTextChange)
        {
            try
            {
                #region Get text change info from xml
                int iSeq = int.Parse(xmlTextChange.SelectSingleNode("/LiveCode/Text/@seq").Value);
                int iUserID = int.Parse(xmlTextChange.SelectSingleNode("/LiveCode/Text/@UserID").Value);

                int iStartLine = int.Parse(xmlTextChange.SelectSingleNode("/LiveCode/Text/@iStartLine").Value);
                int iStartIndex = int.Parse(xmlTextChange.SelectSingleNode("/LiveCode/Text/@iStartIndex").Value);
                int iNewEndLine = int.Parse(xmlTextChange.SelectSingleNode("/LiveCode/Text/@iNewEndLine").Value);
                int iNewEndIndex = int.Parse(xmlTextChange.SelectSingleNode("/LiveCode/Text/@iNewEndIndex").Value);
                int iOldEndLine = int.Parse(xmlTextChange.SelectSingleNode("/LiveCode/Text/@iOldEndLine").Value);
                int iOldEndIndex = int.Parse(xmlTextChange.SelectSingleNode("/LiveCode/Text/@iOldEndIndex").Value);
                string sText = "";
                if (xmlTextChange.SelectSingleNode("/LiveCode/Text/@Text") != null)
                {
                    sText = xmlTextChange.SelectSingleNode("/LiveCode/Text/@Text").Value;
                }
                #endregion

                TextChange change = new TextChange(iUserID, iSeq, iStartLine, iStartIndex, iNewEndLine, iNewEndIndex, iOldEndLine, iOldEndIndex, sText);

                if (!this.dicUserTextSequences.ContainsKey(iUserID))
                {
                    //The first recieved text sequence should be 0
                    this.dicUserTextSequences.Add(iUserID, 0);
                }

                //If the text sequence sent isn't the one expected
                if (this.dicUserTextSequences[iUserID] != iSeq)
                {
                    //Add the text change to the list and wait for the next change to come in (hopefully with the correct sequence)
                    this.lstTextChanges.Add(change);
                    return;
                }

                //Change the text
                this.ChangeText(change);

                //After changes have been applied, increment the text sequence for the user
                this.dicUserTextSequences[iUserID]++;

                #region Check for queued changes
                //After all current changes have been applied check to see if there are any queued changes to do
                if (this.lstTextChanges.Count > 0)
                {
                    List<TextChange> lstUserChanges = this.lstTextChanges.Where(c => c.UserID == iUserID).ToList();
                    //If there are changes to apply for the user
                    if (lstUserChanges.Count > 0)
                    {
                        //While there are still changes to process
                        while (lstUserChanges.Count > 0)
                        {
                            //Get the change with the lowest sequence
                            change = lstUserChanges.Where(ch => ch.Seq == lstUserChanges.Min(c => c.Seq)).ToList()[0];
                            //If the lowest sequence change is the next sequence expected, then process
                            if (change.Seq == this.dicUserTextSequences[iUserID])
                            {
                                //Change the text
                                this.ChangeText(change);
                            }
                        }
                    }
                }
                #endregion
            }
            catch (Exception e)
            {
                throw e;
            }
        }

        private void ChangeText(TextChange change)
        {
            try
            {
                int iCursorLine = -1;
                int iCursorCol = -1;
                this.textView.GetCaretPos(out iCursorLine, out iCursorCol);

                //Added new line
                if (change.StartLine < change.NewEndLine && change.Text == "")
                {
                    this.bIgnoreChange = true;
                    Microsoft.VisualStudio.ErrorHandler.ThrowOnFailure(this.textView.ReplaceTextOnLine(change.StartLine, change.StartIndex, change.OldEndIndex - change.StartIndex, "\n", 1));

                    //Move cursor position so it stays in the same place relative to the text
                    //Added line on the same line
                    if (iCursorLine == change.StartLine && iCursorCol > change.StartIndex)
                    {
                        this.textView.SetCaretPos(iCursorLine + 1, iCursorCol - change.StartIndex);
                    }
                    //Added line on an above line
                    else if (iCursorLine > change.StartLine)
                    {
                        this.textView.SetCaretPos(iCursorLine + 1, iCursorCol);
                    }
                }
                //TODO Fix remove line
                //Removed a line
                else if (change.OldEndLine > change.NewEndLine)
                {
                    //this.bIgnoreChange = true;
                    //Microsoft.VisualStudio.ErrorHandler.ThrowOnFailure(this.textView.ReplaceTextOnLine(change.OldEndLine, 0, 1, "", 0));

                    //Microsoft.VisualStudio.ErrorHandler.ThrowOnFailure(this.textView.SetSelection(change.OldEndLine, 0, change.OldEndLine, 10000));
                    //Microsoft.VisualStudio.ErrorHandler.ThrowOnFailure(this.textView.ClearSelection(0));
                }
                //Added or removed text
                else if (change.Text.Length <= 1)
                {
                    //If replacing a selection
                    if (change.StartIndex != change.OldEndIndex)
                    {
                        this.bIgnoreChanges = true;
                        //Delete selected text
                        Microsoft.VisualStudio.ErrorHandler.ThrowOnFailure(this.textView.ReplaceTextOnLine(change.StartLine, change.StartIndex, change.OldEndIndex - change.StartIndex, "", 0));
                        //Add text
                        Microsoft.VisualStudio.ErrorHandler.ThrowOnFailure(this.textView.ReplaceTextOnLine(change.StartLine, change.StartIndex, 0, change.Text, change.Text.Length));
                        this.bIgnoreChanges = false;
                    }
                    else
                    {
                        this.bIgnoreChange = true;
                        Microsoft.VisualStudio.ErrorHandler.ThrowOnFailure(this.textView.ReplaceTextOnLine(change.StartLine, change.StartIndex, change.OldEndIndex - change.StartIndex, change.Text, change.Text.Length));
                    }

                    //Change the cursor position so it stays in the same place relative to the text
                    //Added text
                    if (iCursorLine == change.StartLine && iCursorCol > change.StartIndex && change.Text.Length > 0)
                    {
                        this.textView.SetCaretPos(iCursorLine, iCursorCol + change.Text.Length);
                    }
                    //Removed text
                    else if (iCursorLine == change.StartLine && iCursorCol > change.StartIndex && change.Text.Length == 0)
                    {
                        //If cursor is within a section of removed text
                        if (iCursorCol <= change.OldEndIndex)
                        {
                            this.textView.SetCaretPos(iCursorLine, change.NewEndIndex);
                        }
                        //If cursor is after removed text
                        else
                        {
                            this.textView.SetCaretPos(iCursorLine, iCursorCol - (change.OldEndIndex - change.StartIndex));
                        }
                    }
                }
                //Added a section of text
                else
                {
                    this.bIgnoreChanges = true;
                    int iStart = change.StartIndex;
                    foreach (char c in change.Text)
                    {
                        Microsoft.VisualStudio.ErrorHandler.ThrowOnFailure(this.textView.ReplaceTextOnLine(change.StartLine, change.StartIndex++, 0, c.ToString(), 1));
                    }
                    this.bIgnoreChanges = false;

                    if (iCursorLine == change.StartLine && iCursorCol > iStart)
                    {
                        this.textView.SetCaretPos(iCursorLine, iCursorCol + change.Text.Length);
                    }
                }
            }
            catch (Exception e)
            {
                throw e;
            }
        }

        private void StartTextEventListening()
        {
            IVsTextManager textManager = Package.GetGlobalService(typeof(SVsTextManager)) as IVsTextManager;

            textManager.GetActiveView(1, null, out this.textView);
            IVsTextLines buffer;
            this.textView.GetBuffer(out buffer);

            IConnectionPointContainer container = (IConnectionPointContainer)buffer;
            if (container != null)
            {
                Guid eventsGuid = typeof(IVsTextLinesEvents).GUID;
                IConnectionPoint connectionPoint;
                uint connectionCookie = 0;
                container.FindConnectionPoint(ref eventsGuid, out connectionPoint);
                connectionPoint.Advise((IVsTextLinesEvents)this, out connectionCookie);
            }
        }

        void IVsTextLinesEvents.OnChangeLineAttributes(int iFirstLine, int iLastLine)
        {
            //TODO figure this thing out, what it is, and if I want it
            //throw new NotImplementedException();
        }

        void IVsTextLinesEvents.OnChangeLineText(TextLineChange[] pTextLineChange, int fLast)
        {
            if (this.bIgnoreChange || this.bIgnoreChanges)
            {
                this.bIgnoreChange = false;
                return;
            }

            //TODO this really needs to be in a separate thread
            int iStartLine = pTextLineChange[0].iStartLine;
            int iStartIndex = pTextLineChange[0].iStartIndex;
            int iNewEndLine = pTextLineChange[0].iNewEndLine;
            int iNewEndIndex = pTextLineChange[0].iNewEndIndex;
            int iOldEndLine = pTextLineChange[0].iOldEndLine;
            int iOldEndIndex = pTextLineChange[0].iOldEndIndex;

            XmlDocument xmlTextChanged = new XmlDocument();
            XmlNode xmlLiveCode = xmlTextChanged.AppendChild(xmlTextChanged.CreateElement("LiveCode"));
            XmlNode xmlText = xmlLiveCode.AppendChild(xmlTextChanged.CreateElement("Text"));

            XmlAttribute xmlSeq = xmlText.Attributes.Append(xmlTextChanged.CreateAttribute("seq"));
            xmlSeq.Value = this.project.TextSeq.ToString();
            this.project.TextSeq++;
            XmlAttribute xmlUserID = xmlText.Attributes.Append(xmlTextChanged.CreateAttribute("UserID"));
            xmlUserID.Value = this.project.User.ID.ToString();
            XmlAttribute xmlRelativePath = xmlText.Attributes.Append(xmlTextChanged.CreateAttribute("RelativePath"));
            xmlRelativePath.Value = this.sRelativePath;
            XmlAttribute xmlStartLine = xmlText.Attributes.Append(xmlTextChanged.CreateAttribute("iStartLine"));
            xmlStartLine.Value = iStartLine.ToString();
            XmlAttribute xmlStartIndex = xmlText.Attributes.Append(xmlTextChanged.CreateAttribute("iStartIndex"));
            xmlStartIndex.Value = iStartIndex.ToString();
            XmlAttribute xmlNewEndLine = xmlText.Attributes.Append(xmlTextChanged.CreateAttribute("iNewEndLine"));
            xmlNewEndLine.Value = iNewEndLine.ToString();
            XmlAttribute xmlNewEndIndex = xmlText.Attributes.Append(xmlTextChanged.CreateAttribute("iNewEndIndex"));
            xmlNewEndIndex.Value = iNewEndIndex.ToString();
            XmlAttribute xmlOldEndLine = xmlText.Attributes.Append(xmlTextChanged.CreateAttribute("iOldEndLine"));
            xmlOldEndLine.Value = iOldEndLine.ToString();
            XmlAttribute xmlOldEndIndex = xmlText.Attributes.Append(xmlTextChanged.CreateAttribute("iOldEndIndex"));
            xmlOldEndIndex.Value = iOldEndIndex.ToString();

            //If an insert or replace
            if ((iNewEndIndex > iOldEndIndex) || (iStartIndex != iOldEndIndex && iStartIndex != iNewEndIndex))
            {
                //Get text
                string sText = "";
                this.textView.GetTextStream(iStartLine, iStartIndex, iNewEndLine, iNewEndIndex, out sText);

                XmlAttribute xmlTextChange = xmlText.Attributes.Append(xmlTextChanged.CreateAttribute("Text"));
                xmlTextChange.Value = sText;
            }

            this.project.User.IOHandler.QuickSend(xmlTextChanged);
        }
    }
}
